package src;

import jargs.gnu.CmdLineParser.IllegalOptionValueException;
import jargs.gnu.CmdLineParser.UnknownOptionException;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import lib.ArgumentValidator;
import lib.Utility;
import lib.logging.ANSIConsoleLogger;
import lib.logging.ILogger;
import lib.messaging.Communicator;
import lib.messaging.IReceiver;
import lib.messaging.ISender;
import lib.messaging.NetworkAddress;
import lib.paxos.AcceptorState;
import lib.paxos.IAcceptorState;
import lib.paxos.PaxosFunctionality;
import lib.rpc.ClientRPCFunctionality;
import lib.rpc.ClientRPCMethods;
import lib.rpc.IRPCMethods;
import lib.rpc.RPCInvokePacket;
import lib.rpc.RPCResultPacket;
import lib.rpc.ServerRPCMethods;

/**
 * Server for accepting requests from client
 */
public class Server
{
    /**
     * Main method of Server
     *
     * @param args
     * @throws UnknownOptionException
     * @throws IllegalOptionValueException
     */
    public static void main(String[] args) throws IllegalOptionValueException, UnknownOptionException
    {
        ServerConfiguration serverConfig = ServerConfiguration.ReadConfiguration(args);
        Server server = new Server(serverConfig);
        server.Begin();
    }

    private IAcceptorState acceptorState;
    private IRPCMethods clientRPCMethods;
    private ILogger consoleLogger;
    private NetworkAddress existingServerAddress;
    private boolean joinExistingServer;
    private ILogger logger;
    private NetworkAddress serverAddress;
    private double serverPacketDropProbability;
    private File serverRootDirectory;
    private File acceptorStateDumpFile;
    private IServerRPCHandler serverRPCHandler;

    public Server(ServerConfiguration serverConfig)
    {
        ArgumentValidator.IsNotNull("serverConfig", serverConfig);
        ArgumentValidator.IsGreaterThan("otherServerAddresses.size", serverConfig.OtherServerAddresses().size(), 0);

        this.logger = new ANSIConsoleLogger();
        this.consoleLogger = new ANSIConsoleLogger();
        this.serverRootDirectory = ServerConfiguration.ServerRootDirectory;
        this.acceptorState = new AcceptorState(
                serverConfig.ServerAddress(),
                this.logger,
                serverConfig.OtherServerAddresses());
        this.serverAddress = serverConfig.ServerAddress();
        this.serverPacketDropProbability = serverConfig.ServerPacketDropProbability();
        this.joinExistingServer = serverConfig.JoinExistingServer();
        this.existingServerAddress = serverConfig.ExistingServerAddress();

        ServerRPCMethods serverRPCMethods = new ServerRPCMethods(
                serverConfig.ServerAddress(),
                this.serverRootDirectory,
                this.acceptorState,
                this.logger);

        this.clientRPCMethods = new ClientRPCMethods(
                this.logger,
                new ClientRPCFunctionality(
                        this.logger,
                        ServerConfiguration.Timeout,
                        serverConfig.ServerPacketDropProbability()));

        this.serverRPCHandler = new ServerRPCHandler(
                this.serverAddress,
                serverRPCMethods,
                new PaxosFunctionality(
                        this.logger,
                        serverConfig.ServerAddress(),
                        serverRPCMethods,
                        this.clientRPCMethods,
                        acceptorState,
                        serverConfig.ReconfigureAutomatically()));

        this.acceptorStateDumpFile = new File(
                Utility.GetServerDirectory(
                        this.serverRootDirectory,
                        this.serverAddress),
                String.format(
                        ServerConfiguration.AcceptorStateDumpFileNameFormatString,
                        this.serverAddress.Name(),
                        this.serverAddress.Port()));
    }

    private List<NetworkAddress> AttemptToJoinServers()
    {
        IRPCMethods clientRPCMethodsForJoin = new ClientRPCMethods(
                this.logger,
                new ClientRPCFunctionality(
                        this.logger,
                        0,
                        this.serverPacketDropProbability));
        while (true)
        {
            consoleLogger.LogInfo("Enter roundID to be used for join request:");
            String roundIDString = System.console().readLine();
            int roundID = Integer.parseInt(roundIDString);

            try
            {
                List<NetworkAddress> allServers =  clientRPCMethodsForJoin.Join(
                        this.serverAddress,
                        this.existingServerAddress,
                        roundID,
                        this.serverAddress);

                if (allServers != null)
                {
                    return allServers;
                }

                logger.LogError("Join request failed. Please retry...");
            }
            catch (ClassNotFoundException e)
            {
                logger.LogException(e);
            }
            catch (IOException e)
            {
                logger.LogException(e);
            }
        }
    }

    private void Begin()
    {
        logger.LogInfo("Server with ID:%d starting up...", serverAddress.Port());
        logger.LogInfo("Deleting all previous state!");
        DeletePreviousState();

        if (this.joinExistingServer)
        {
            List<NetworkAddress> allServers = AttemptToJoinServers();

            Collections.sort(allServers);
            logger.LogInfo(
                    "Successfully joined existing servers. List of all known servers: %s",
                    Utility.FlattenListToString(allServers));

            PopulateOtherServerAddresses(allServers);
        }

        while (true)
        {
            try
            {
                logger.LogInfo("Waiting for connection on %s ...", serverAddress.toString());

                RPCInvokePacket rpcInvokePacket = ReceiveInvokePacket(serverAddress.Port());

                logger.LogInfo("Received RPCInvokePacket: %s",
                        rpcInvokePacket.toString());
                RPCResultPacket rpcResultPacket = serverRPCHandler.HandleRPC(rpcInvokePacket);

                logger.LogInfo("Sending RPCResultPacket: %s",
                        rpcResultPacket.toString());
                SendRPCResult(rpcInvokePacket.ClientAddress(), rpcResultPacket);

                logger.LogInfo("Dumping acceptor state to %s.", acceptorStateDumpFile.getAbsolutePath());
                this.acceptorState.DumpAcceptorStateToFile(acceptorStateDumpFile);
            }
            catch (ClassNotFoundException e)
            {
                logger.LogException(e);
            }
            catch (IOException e)
            {
                logger.LogException(e);
            }
            catch (UnsupportedOperationException e)
            {
                logger.LogException(e);
            }
        }
    }

    private void DeleteFilesRecursively(File f)
    {
        if (f.isDirectory())
        {
            for (File c : f.listFiles())
            {
                DeleteFilesRecursively(c);
            }
        }

        logger.LogInfo("Deleting file:%s", f.getAbsoluteFile());
        f.delete();
    }

    private void DeletePreviousState()
    {
        File serverDir = Utility.GetServerDirectory(serverRootDirectory, serverAddress);

        if (!serverDir.exists())
        {
            return;
        }

        DeleteFilesRecursively(serverDir);
    }

    private void PopulateOtherServerAddresses(List<NetworkAddress> allServers)
    {
        List<NetworkAddress> otherServers = new ArrayList<NetworkAddress>();
        for (int i = 0; i < allServers.size(); ++i)
        {
            if (!allServers.get(i).equals(this.serverAddress))
            {
                otherServers.add(allServers.get(i));
            }
        }

        this.acceptorState.SetOtherServerAddresses(otherServers);
    }

    private RPCInvokePacket ReceiveInvokePacket(int port)
            throws ClassNotFoundException, IOException
    {
        IReceiver receiver = new Communicator(
                this.serverPacketDropProbability,
                logger);
        byte[] dataReceived = receiver.ReceiveBytes(port);

        return RPCInvokePacket.Deserialize(dataReceived);
    }

    private void SendRPCResult(
            NetworkAddress clientAddress,
            RPCResultPacket rpcResultPacket) throws IOException
    {
        ISender sender = new Communicator(
                this.serverPacketDropProbability,
                logger);
        sender.SendBytes(clientAddress, rpcResultPacket.Serialize());
    }
}
